我是後端的初學者,所以通常會先寫測試讓自己比較熟悉,另外,在開發過程中立即運行測試,也就是完成一個邏輯先跑測試,這樣可以及早發現和修復錯誤,避免在後期修復時增加的工作量和複雜性。
以下主要是依照第 23 天:代碼質量與測試 - 寫測試的流程除了基本規劃以外,進入開發前先寫測試。
今天和明天的任務主要是開後台 api ,主要運用 CRUD 操作來實現後台任務管理應用基本功能
我自己在前端的時候評估任務時程都會拆小的任務包,內容就是確認重要需求以及任務可能會使用的技能,最重要的是...會不會有機會撞牆!?
站在後端角色做一個後台任務管理應用,用到基本的 CRUD ,可以參考前輩的商品後台,另外,站在使用者體驗的角度觀察,如果我是一個後台管理者,我會需要哪些功能?
需要開的 api 明細
no | 項目 | 方法 | 說明 | 請求驗證 |
---|---|---|---|---|
1 | 任務列表 | GET |
顯示所有任務 | 前端不用帶參數 |
2 | 創建任務 | POST |
新增一個任務 | 前端依照需求帶參數存入資料庫 |
3 | 更新任務 | PUT |
修改現有的任務 | 前端路由中帶 task_id |
4 | 更新任務 | PETCH |
修改現有任務的一小部分 | 前端路由中帶 task_id 和布林值 |
5 | 刪除任務 | DELECT |
刪除一個任務 | 前端路由中帶 task_id |
過去前端的經驗告訴我:身為後端要做好溝通的橋樑,開發前除了想一下表格架構以外,也可以找雙方討論交流回傳格式,避免前端收到太大的驚喜!
圖片來源:(專案倒數1天) 你需要的API我都做完了唷!加油!
第一步會先做建立路由和閉包的方法,主要是要確保給前端的架構和 HTTP 狀態必須符合當初的溝通結果,因為這裡開始就是要回傳給前端的關鍵路徑,其他進入 service 和 repository 的部分就是後端自己的事情,這裡最重要的就是回傳是要正確的,其他後端自己處理即可!
<?php
use App\Http\Controllers\TaskController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::prefix('tasks')->name('tasks.')->group(function () {
Route::post('/', [TaskController::class, 'storeTask'])->name('create');
Route::put('/{task}', [TaskController::class, 'updateTask'])->name('update');
Route::patch('/{task}/{completed}', [TaskController::class, 'changeTaskComplete'])->name('change.complete');
Route::delete('/{task}', [TaskController::class, 'deleteTask'])->name('delete');
Route::get('/', [TaskController::class, 'showTasks'])->name('show');
});
不過,寫測試之前先做工廠建立,因為要做比對用的!
建立工廠
指令 php artisan make:factory TaskFactory --model=Task
,然後在 database/factories/TaskFactory.php
中定義工廠的欄位。
<?php
namespace Database\Factories;
use App\Models\Task;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* 任務工廠
*
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Task>
*/
class TaskFactory extends Factory
{
protected $model = Task::class;
/**
* 定義模型的預設狀態。
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
return [
'title' => $this->faker->sentence,
'description' => $this->faker->paragraph,
'completed' => $this->faker->boolean,
'created_at' => now(),
'updated_at' => now(),
];
}
}
寫測試 step by step
首先這裡知道要驗的是 url & controller,所以依照第 23 天:代碼質量與測試 - 寫測試的流程執行。
<?php
namespace Tests\Feature;
use App\Models\Task;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/**
* 測試後台任務管理應用控制器
*
*/
class TaskControllerTest extends TestCase
{
use RefreshDatabase;
/**
* 測試取得所有任務明細
*
* @return void
*/
public function testShowTasks(): void
{
// 工廠建資料
Task::factory()->count(count: 3)->create();
// 跑結果 & 比對
$this->get(uri: '/api/tasks')
->assertOk()
->assertJsonCount(count: 3);
}
/**
* 測試新增任務
*
* @return void
*/
public function testStoreTask(): void
{
$data = [
"title" => "new_task",
"description" => "test",
"completed" => true,
];
$this->post(uri: '/api/tasks', data: $data)
->assertStatus(status: 201)
->assertJson(value: $data);
$this->assertDatabaseHas(table: 'tasks', data: $data);
}
/**
* 測試編輯任務
* @return void
*/
public function testUpdateTask(): void
{
$task = Task::factory()->create();
$data = [
'title' => 'Updated Task',
"description" => "test",
"completed" => true,
];
$this->put(uri: "/api/tasks/{$task->id}", data: $data)
->assertOk()
->assertJson(value: $data);
$this->assertDatabaseHas(table: 'tasks', data: $data);
}
/**
* 測試改變任務的完成欄位
*
* @return void
*/
public function testChangeTaskComplete(): void
{
$task = Task::factory()->create(attributes: ['completed' => true]);
$this->patch(uri: "/api/tasks/{$task->id}/{$task->completed}")
->assertOk();
}
/**
* 測試刪除任務
*
* @return void
*/
public function testDeleteTask(): void
{
$task = Task::factory()->create();
$this->delete("/api/tasks/{$task->id}")
->assertOk();
}
}
單元測試主要分成 service、repository、transformer,甚至是 request 也可以,這邊主要先以 service、repository 為主!
進入單元測試後,一定會很常遇到過不了的問題,因為邏輯只寫了一半,所以難免會需要回頭改,但是至少註解寫上後知道要多注意哪些細節,避免壓線的時候才發現 bug 或是需求沒有完善!開始吧!
service
指令 php artisan make:test TaskServiceTest --unit
,預期都是過水,大部分都直接傳入 repository 做 CRUD,所以這裡做一個測試建立任務的邏輯處理
<?php
namespace Tests\Unit;
use App\Models\Task;
use App\Repositories\TaskRepository;
use App\Services\TaskService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Mockery\LegacyMockInterface;
use Mockery\MockInterface;
use Tests\TestCase;
class TaskServiceTest extends TestCase
{
use RefreshDatabase;
protected LegacyMockInterface|MockInterface $mock_repository;
protected TaskService $service;
protected function setUp(): void
{
parent::setUp();
$this->mock_repository = Mockery::mock(TaskRepository::class);
$this->service = new TaskService($this->mock_repository);
}
/**
* 測試新增任務的邏輯處理
*
* @return void
*/
public function testStoreTask(): void
{
// 設定測試資料
$data = [
'title' => 'New Task',
'description' => 'Task description',
'completed' => false,
];
// 用工廠建立預期結果
$expected = Task::factory()->make($data);
// 用 mock 開始模擬,調用方法並且執行一次並且回傳前面建立的預設值
$this->mock_repository->shouldReceive('storeTask')
->once()
->andReturn($expected);
// 調用被測試的方法
$activity = $this->service->storeTask($data);
// 斷言比對
$this->assertEquals($expected, $activity);
}
}
repository
指令 php artisan make:test TaskRepositoryTest --unit
<?php
namespace Tests\Unit;
use App\Models\Task;
use App\Repositories\TaskRepository;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class TaskRepositoryTest extends TestCase
{
use RefreshDatabase;
protected TaskRepository $repository;
protected function setUp(): void
{
parent::setUp();
$this->repository = app()->make(TaskRepository::class);
}
/**
* 測試取得所有任務明細的資料處理
*
* @return void
*/
public function testShowTasks(): void
{
Task::factory()->count(3)->create();
$this->assertDatabaseCount(Task::class, 3);
}
/**
* 測試新增任務的資料處理
*
* @return void
*/
public function testStoreTask(): void
{
// 預設
$data = [
'title' => 'New Task',
'description' => 'Task description',
'completed' => false,
];
// 模擬跑出來的真值
$task = $this->repository->storeTask($data);
// 斷言
$this->assertDatabaseHas(Task::class, $data);
$this->assertEquals($data['title'], $task->title);
}
/**
* 測試編輯任務的資料處理
*
* @return void
*/
public function testUpdateTask(): void
{
// 預設
$task = Task::factory()->create();
$data = [
'title' => 'Updated Task',
'description' => 'Updated description',
'completed' => true,
];
// 模擬跑出來的真值
$updatedTask = $this->repository->updateTask($task, $data);
// 斷言
$this->assertEquals(expected: $data['title'], actual: $updatedTask->title);
$this->assertDatabaseHas(Task::class, $data);
}
/**
* 測試改變任務的完成欄位資料處理
*
* @return void
*/
public function testChangeTaskComplete(): void
{
// 預設
$task = Task::factory()->create(['completed' => false]);
// 模擬跑出來的真值
$this->repository->changeTaskComplete($task, true);
// 斷言
$this->assertDatabaseHas(table: Task::class, data: ['id' => $task->id, 'completed' => true]);
$this->assertTrue($task->fresh()->completed);
}
/**
* 測試刪除任務的資料處理
*
* @return void
*/
public function testDeleteTask(): void
{
// 預設
$task = Task::factory()->create();
// 模擬跑出來的真值
$this->repository->deleteTask($task);
// 斷言
$this->assertDatabaseMissing(Task::class, ['id' => $task->id]);
}
}